export interface MetaActionNode {
  actionKey: string;
  args: any[];
}

export interface ActionNode<T> {
  tier: number;
  actionKey: string;
  args: any[];
  lastActionNode: ActionNode<unknown> | null;
  nextActionNodes: ActionNode<unknown>[];
  metaActionNodes: MetaActionNode[];
  recycler: T;
}

export type GetterOption<S, R> = (state: S) => R;

export type ActionOption<S, A extends any[], T> = {
  do: (this: ActionNode<T>, state: S, ...args: A) => void;
  undo?: (this: ActionNode<T>, state: S, ...args: A) => void;
};

export interface ActtreeOptions<S, GS, AS> {
  state: (() => S) | S;
  getters?: GS;
  actions?: AS;
  tierLimit?: number;
  parallLimit?: number;
}

class SmartProxy {
  static actions = {
    set: {
      do(target, p, oldValue, newValue) {
        target[p] = newValue;
      },
      undo(target, p, oldValue, newValue) {
        target[p] = oldValue;
      },
    },
    deleteProperty: {
      do(target, p, oldValue) {
        delete target[p];
      },
      undo(target, p, oldValue) {
        target[p] = oldValue;
      },
    },
    addProperty: {
      do(target, p, newValue) {
        target[p] = newValue;
      },
      undo(target, p, newValue) {
        delete target[p];
      },
    },
  };
  static signKey = "_IsSmartProxy_";
  static targetKey = "_SmartTarget_";
  static undo(actionNode: ActionNode<unknown>) {
    const metaActionNodes = actionNode.metaActionNodes;
    for (let i = metaActionNodes.length - 1; i >= 0; i--) {
      const metaActionNode = metaActionNodes[i];
      const action = this.actions[metaActionNode.actionKey];
      action.undo(...metaActionNode.args);
    }
  }
  static redo(actionNode: ActionNode<unknown>) {
    const metaActionNodes = actionNode.metaActionNodes;
    for (let i = 0; i < metaActionNodes.length; i++) {
      const metaActionNode = metaActionNodes[i];
      const action = this.actions[metaActionNode.actionKey];
      action.do(...metaActionNode.args);
    }
  }
  //创建属性操作拦截对象
  static createProxy(actionNode: ActionNode<unknown>, obj: any) {
    if (typeof obj == "object" && obj != null) {
      return new Proxy(obj, {
        get: (target, p, receiver) => {
          if (p == this.signKey) {
            return true;
          } else if (p == this.targetKey) {
            return target;
          } else {
            return this.createProxy(actionNode, target[p]);
          }
        },
        set: (target, p, value, receiver) => {
          const hasOld = p in target;
          const oldValue = target[p];
          const autoNodes: MetaActionNode[] = [];
          let metaActionNode: MetaActionNode | null = null;
          if (hasOld) {
            if (Array.isArray(target) && p == "length" && value < oldValue) {
              for (let i = oldValue - 1; i >= value; i--) {
                autoNodes.push({
                  actionKey: "deleteProperty",
                  args: [target, i, target[i]],
                });
              }
            }
            metaActionNode = {
              actionKey: "set",
              args: [target, p, oldValue, value],
            };
          } else {
            const index =
              typeof p == "number"
                ? p
                : typeof p == "string"
                ? parseInt(p)
                : NaN;
            if (
              Array.isArray(target) &&
              !Number.isNaN(index) &&
              index >= target.length
            ) {
              autoNodes.push({
                actionKey: "set",
                args: [target, "length", target.length, index + 1],
              });
            }
            metaActionNode = {
              actionKey: "addProperty",
              args: [target, p, value],
            };
          }
          let result = true;
          try {
            if (value && value[this.signKey]) {
              target[p] = value[this.targetKey];
            } else {
              target[p] = value;
            }
          } catch (e) {
            result = false;
          }
          if (result) {
            for (let autoNode of autoNodes) {
              actionNode.metaActionNodes.push(autoNode);
            }
            actionNode.metaActionNodes.push(metaActionNode);
          }
          return result;
        },
        deleteProperty: (target, p) => {
          let oldValue = target[p];
          let metaActionNode = {
            actionKey: "deleteProperty",
            args: [target, p, oldValue],
          };
          let result = true;
          try {
            delete target[p];
          } catch (e) {
            result = false;
          }
          if (result) {
            actionNode.metaActionNodes.push(metaActionNode);
          }
          return result;
        },
      });
    } else {
      return obj;
    }
  }
}

export class Acttree<
  S,
  GS extends Record<string, GetterOption<S, any>>,
  AS extends Record<string, ActionOption<S, any[], any>>
> {
  private options: ActtreeOptions<S, GS, AS>;
  private nowActionNode: ActionNode<any>;
  private rootActionNode: ActionNode<any>;
  private listeners = {
    do: [],
    undo: [],
    redo: [],
    tierOverflow: [],
    parallOverflow: [],
  };
  /** 视图 */
  getters: {
    readonly [key in keyof GS]: ReturnType<GS[key]>;
  };
  /** 状态 */
  get state(): S {
    const op_state = this.options.state;
    if (typeof op_state === "function") {
      return (op_state as () => S)();
    } else {
      return op_state;
    }
  }
  /** 纵向最大层数 */
  get tierLimit() {
    return this.options.tierLimit ?? 1000;
  }
  /** 横向最大层数 */
  get parallLimit() {
    return this.options.parallLimit ?? 5;
  }
  constructor(options: ActtreeOptions<S, GS, AS>) {
    this.options = options;
    this.getters = new Proxy(options.getters, {
      ownKeys: (target) => {
        return Reflect.ownKeys(target);
      },
      get: (target, key) => {
        return target[key as string](this.state);
      },
      set: (target, key, value) => {
        throw new Error("getters is readonly");
      },
    }) as {
      readonly [key in keyof GS]: ReturnType<GS[key]>;
    };
    this.nowActionNode = this.rootActionNode = {
      tier: 0,
      actionKey: "_root_",
      args: [],
      lastActionNode: null,
      nextActionNodes: [],
      metaActionNodes: [],
      recycler: null,
    };
  }
  nowTier() {
    return this.nowActionNode.tier;
  }
  undoable() {
    return this.nowActionNode != this.rootActionNode;
  }
  redoLength() {
    return this.nowActionNode.nextActionNodes.length;
  }
  isFull() {
    return (
      this.tierLimit > 0 &&
      this.nowActionNode.tier - this.rootActionNode.tier > this.tierLimit
    );
  }
  isEmpty() {
    return (this.nowActionNode = this.rootActionNode);
  }
  recover(tier: number) {
    tier = tier || this.rootActionNode.tier;
    while (
      this.nowActionNode.tier > this.rootActionNode.tier &&
      this.nowActionNode.tier > tier
    ) {
      this.undo();
    }
  }
  emptying() {
    if (!this.isEmpty()) {
      let actionNode = this.nowActionNode;
      for (let node of actionNode.nextActionNodes) {
        node.lastActionNode = this.rootActionNode;
      }
      this.rootActionNode.tier = actionNode.tier;
      this.rootActionNode.nextActionNodes = actionNode.nextActionNodes;
      actionNode.lastActionNode = null;
      actionNode.nextActionNodes = [];
    }
  }
  addListener(
    type: keyof typeof this.listeners,
    listener:
      | string
      | ((this: typeof this, actionNode: ActionNode<unknown>) => void)
  ) {
    let list = this.listeners[type] || [];
    list.push(listener);
    this.listeners[type] = list;
  }
  removeListener(
    type: keyof typeof this.listeners,
    listener:
      | string
      | ((this: typeof this, actionNode: ActionNode<unknown>) => void)
  ) {
    let list = this.listeners[type] || [];
    list.push(listener);
    this.listeners[type] = list;
  }
  do<K extends keyof AS & string>(
    actionKey: K,
    ...args: Parameters<AS[K]["do"]>
  ) {
    const actionNode: ActionNode<unknown> = {
      tier: -1,
      actionKey,
      args,
      lastActionNode: null,
      nextActionNodes: [],
      metaActionNodes: [],
      recycler: null,
    };
    const actionNodes = this.nowActionNode.nextActionNodes;
    actionNodes.push(actionNode);
    if (this.parallLimit > 0 && actionNodes.length > this.parallLimit) {
      let actionNode = actionNodes.shift();
      actionNode.lastActionNode = null;
      this.listeners.parallOverflow.forEach((listener) => {
        if (typeof listener == "function") {
          listener.call(this, actionNode);
        } else if (typeof listener == "string") {
          (function (_code_, actionNode) {
            eval(_code_);
          }).call(this, listener, actionNode);
        }
      });
    }
    this.nowActionNode = actionNode;
    if (this.isFull()) {
      let actionNode = this.nowActionNode;
      while (actionNode.lastActionNode != this.rootActionNode) {
        actionNode = actionNode.lastActionNode;
      }
      for (let node of actionNode.nextActionNodes) {
        node.lastActionNode = this.rootActionNode;
      }
      this.rootActionNode.tier = actionNode.tier;
      this.rootActionNode.nextActionNodes = actionNode.nextActionNodes;
      actionNode.lastActionNode = null;
      actionNode.nextActionNodes = [];
      this.listeners.tierOverflow.forEach((listener) => {
        if (typeof listener == "function") {
          listener.call(this, actionNode);
        } else if (typeof listener == "string") {
          (function (_code_, actionNode) {
            eval(_code_);
          }).call(this, listener, actionNode);
        }
      });
    }
    let actionName = actionKey;
    let action = this.options.actions[actionName];
    if (action) {
      if (action.undo) {
        action.do.call(actionNode, this.state, ...actionNode.args);
      } else {
        action.do.call(
          actionNode,
          SmartProxy.createProxy(actionNode, this.state),
          ...actionNode.args
        );
      }
    }
    this.listeners.do.forEach((listener) => {
      if (typeof listener == "function") {
        listener.call(this, actionNode);
      } else if (typeof listener == "string") {
        (function (_code_, actionNode) {
          eval(_code_);
        }).call(this, listener, actionNode);
      }
    });
  }
  undo() {
    let actionNode = this.nowActionNode;
    if (actionNode != this.rootActionNode) {
      let actionKey = actionNode.actionKey;
      let actionName = actionKey;
      let action = this.options.actions[actionName];
      if (action) {
        if (action.undo) {
          action.undo.call(actionNode, this.state, ...actionNode.args);
        } else {
          SmartProxy.undo(actionNode);
        }
      }
      this.nowActionNode = this.nowActionNode.lastActionNode;
      this.listeners.undo.forEach((listener) => {
        if (typeof listener == "function") {
          listener.call(this, actionNode);
        } else if (typeof listener == "string") {
          (function (_code_, actionNode) {
            eval(_code_);
          }).call(this, listener, actionNode);
        }
      });
      return true;
    } else {
      return false;
    }
  }
  redo(index: number = undefined) {
    if (this.nowActionNode) {
      let actionNodes = this.nowActionNode.nextActionNodes;
      let actionNode = actionNodes[index];
      if (!actionNode) {
        actionNode = actionNodes[actionNodes.length - 1];
      }
      if (actionNode) {
        this.nowActionNode = actionNode;
        let actionKey = actionNode.actionKey;
        let actionName = actionKey;
        let action = this.options.actions[actionName];
        if (action) {
          if (action.undo) {
            action.do.call(actionNode, this.state, ...actionNode.args);
          } else {
            action.do.call(
              actionNode,
              SmartProxy.createProxy(actionNode, this.state),
              ...actionNode.args
            );
          }
        }
        this.listeners.redo.forEach((listener) => {
          if (typeof listener == "function") {
            listener.call(this, actionNode);
          } else if (typeof listener == "string") {
            (function (_code_, actionNode) {
              eval(_code_);
            }).call(this, listener, actionNode);
          }
        });
        return true;
      } else {
        return false;
      }
    }
  }
}

export function createStore<
  S,
  GS extends Record<string, GetterOption<S, any>>,
  AS extends Record<string, ActionOption<S, any[], any>>
>(options: ActtreeOptions<S, GS, AS>) {
  return new Acttree<S, GS, AS>(options);
}
